<?php
/**
 * PHP Password Library
 *
 * @package PHPassLib\Hashes
 * @category Cryptography
 * @author Ryan Chouinard <rchouinard at gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.html MIT License
 * @link https://github.com/rchouinard/phpass Project at GitHub
 */

namespace PHPassLib\Hash\Adapter;

use PHPassLib\Hash\Adapter;
use PHPassLib\Exception\InvalidArgumentException;
use PHPassLib\Exception\RuntimeException;

/**
 * Hash adapter base class
 *
 * @package PHPassLib\Hashes
 * @category Cryptography
 * @author Ryan Chouinard <rchouinard at gmail.com>
 * @license http://www.opensource.org/licenses/mit-license.html MIT License
 * @link https://github.com/rchouinard/phpass Project at GitHub
 */
abstract class Base implements Adapter
{

    /**
     * Flag indicating whether an exception should be thrown on failure.
     *
     * @var unknown_type
     */
    protected $_throwExceptionOnFailure = false;

    /**
     * Alphabet used in itoa64 conversions.
     *
     * @var string
     */
    protected $_itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

    /**
     * Cached random data.
     *
     * This value is used when better methods of generating random data are
     * unavailable.
     *
     * @var string
     */
    protected $_randomState;

    /**
     * Class constructor.
     *
     * @param Array $options
     *   Associative array of adapter options.
     * @return void
     *   Returns nothing; it's a constructor.
     * @see self::setOptions()
     */
    public function __construct(Array $options = array ())
    {
        $this->_randomState = microtime();
        if (function_exists('getmypid')) {
            $this->_randomState .= getmypid();
        }

        $this->setOptions($options);
    }

    /**
     * Return a hashed string.
     *
     * @param string $password
     *   The string to be hashed.
     * @param string $salt
     *   An optional salt string to base the hashing on. If not provided, a
     *   suitable string is generated by the adapter.
     * @return string
     *   Returns the hashed string. On failure, a standard crypt error string
     *   is returned which is guaranteed to differ from the salt.
     * @throws RuntimeException
     *   A RuntimeException is thrown on failure if
     *   self::$_throwExceptionOnFailure is true.
     */
    public function crypt($password, $salt = null)
    {
        if (!$salt) {
            $salt = $this->genSalt();
        }
        $hash =  crypt($password, $salt);

        // XXX: Work around https://bugs.php.net/bug.php?id=61852
        if (!$this->verifyHash($hash)) {
            $hash = ($salt != '*0') ? '*0' : '*1';
            if ($this->_throwExceptionOnFailure) {
                throw new RuntimeException('Failed generating a valid hash', $hash);
            }
        }

        return $hash;
    }

    /**
     * Set adapter options.
     *
     * Expects an associative array of option keys and values used to configure
     * this adapter.
     *
     * <dl>
     *   <dt>throwExceptionOnFailure</dt>
     *     <dd>If true, the crypt() method will throw a RuntimeException on
     *     failure instead of returning a standard crypt error string. Defaults
     *     to false.</dd>
     * </dl>
     *
     * @param Array $options
     *   Associative array of adapter options.
     * @return self
     *   Returns an instance of self to support method chaining.
     */
    public function setOptions(Array $options)
    {
        $options = array_change_key_case($options, CASE_LOWER);

        foreach ($options as $key => $value) {
            switch ($key) {
                case 'throwexceptiononfailure':
                    $this->_throwExceptionOnFailure = (bool) $value;
                    break;
                default:
                    break;
            }
        }

        return $this;
    }

    /**
     * Encode raw data to characters in the itoa64 alphabet.
     *
     * @param string $input
     *   Raw binary data to encode.
     * @param integer $count
     *   Number of bytes to encode.
     * @return string
     *   Returns the encoded data as a string.
     */
    protected function _encode64($input, $count)
    {
        $output = '';
        $i = 0;
        do {
            $value = ord($input[$i++]);
            $output .= $this->_itoa64[$value & 0x3f];
            if ($i < $count) {
                $value |= ord($input[$i]) << 0x08;
            }
            $output .= $this->_itoa64[($value >> 0x06) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            if ($i < $count) {
                $value |= ord($input[$i]) << 0x10;
            }
            $output .= $this->_itoa64[($value >> 0x0c) & 0x3f];
            if ($i++ >= $count) {
                break;
            }
            $output .= $this->_itoa64[($value >> 0x12) & 0x3f];
        } while ($i < $count);

        return $output;
    }

    /**
     * Check if a salt or hash string is valid for the current adapter.
     *
     * @since 2.1.0
     * @param string $input
     *   Salt or hash string to verify.
     * @return boolean
     *   Returns true if the input string is either a valid salt or hash
     *   string, false otherwise.
     */
    public function verify($input)
    {
        return ($this->verifyHash($input) || $this->verifySalt($input));
    }

    /**
     * Generate a pseudo-random string of bytes.
     *
     * @param integer $count
     *   The length of the desired string of bytes. Must be a positive integer.
     * @return string
     *   Returns the generated string of bytes.
     * @throws InvalidArgumentException
     *   Thows an InvalidArgumentException if the $count parameter is not a
     *   positive integer.
     */
    protected function _getRandomBytes($count)
    {
        if (!is_int($count) || $count < 1) {
            throw new InvalidArgumentException('Argument must be a positive integer');
        }

        // Try OpenSSL's random generator
        if (function_exists('openssl_random_pseudo_bytes')) {
            $strongCrypto = false;
            $output = openssl_random_pseudo_bytes($count, $strongCrypto);
            if ($strongCrypto && strlen($output) == $count) {
                return $output;
            }
        }

        // Try reading from /dev/urandom, if present
        $output = '';
        if (is_readable('/dev/urandom') && ($fh = fopen('/dev/urandom', 'rb'))) {
            $output = fread($fh, $count);
            fclose($fh);
        }

        // Fall back to a locally generated "random" string
        if (strlen($output) < $count) {
            $output = '';
            for ($i = 0; $i < $count; $i += 16) {
                $this->_randomState = md5(microtime() . $this->_randomState);
                $output .= md5($this->_randomState, true);
            }
            $output = substr($output, 0, $count);
        }

        return $output;
    }

}
